Scala a functional approch 3 : Ticket management system

Abstract

This document is a efford to introduce the strengths and benefits of functional programming in scala.

We do not claim intellectual property of all the material presented. We specifically refer to the original resources whenever is needed.

The presentation path of the concepts is still under consideration and may be changed in future reviews.

Outline

Example heavily influenced by Functional and Reactive Domain Modeling

Ticket domain algebra


In [1]:
//Model as an ADT


type Comments = Seq[String]
val emptyComments = Seq.empty[String]


sealed trait TicketStatus
case object Open extends TicketStatus
case object InProgress extends TicketStatus
case object Closed extends TicketStatus

//Aggregate root
case class Ticket(no:String, status: TicketStatus, title: String, comments: Comments)


defined type Comments
emptyComments: Seq[String] = List()
defined trait TicketStatus
defined object Open
defined object InProgress
defined object Closed
defined class Ticket

A simple domain service


In [28]:
// Service attempt #1

//Let's define a service that describes our algebra

trait TicketService {
  
  //Open a ticket
  def open(no: String, title: String): Ticket
  
  //Make a ticket in progress
  def start(no: String): Ticket 
  
  //Change the title
  def changeTitle(no: String, title: String): Ticket 
  
  //Close ticket
  def close(no: String): Ticket
}

//What we can do with this algebra?

//Open a ticket with no = "t1", title ="..." 
// start this ticket
// update this ticket title = "...."
// close this ticket 
def program1(ts: TicketService): Ticket = {
  val t =  ts.open("t1", "...") //Every step returns an immutable ticket with the changes
  val t1 = ts.start(t.no)
  val t2 = ts.changeTitle(t1.no, "Ticket updated")
  val t3 = ts.close(t2.no)
  t3
}

program1 _


defined trait TicketService
defined function program1
res27_2: $user.TicketService => Ticket = <function1>

In [ ]:
//Composition basics

//Methods
def f(n: String) = n + n
def g(n: String) = n.length

//Functions
// (String => String)
val ff = (n: String) => n + n

// (String => Int)
val gf = (n: String) => n.length

//Composition
val gof = (g _).compose(f _)

val gof1 = (f _).andThen(g _)

//Composition with lambda
val folambda = (g _).compose((x:String) => x + x + x)

f("1")
gof("1")
folambda("foo")

In [ ]:
//Service attempt #1.1
//Given that lets re-write our program

def program1(ts: TicketService): Ticket = {
  val chain = (ts.open((_:String), "..."))
      .andThen(t => ts.start(t.no))
      .andThen(t => ts.changeTitle(t.no, "...."))
      .andThen(t => ts.close(t.no))
   chain("t1")
}

program1 _

Define a ticket repository interface


In [ ]:
trait TicketRepository {
  def query(no: String): Ticket
  def store(t: Ticket): Ticket  
}

Using the TicketRepository in the TicketService.


In [ ]:
//Service attempt #2

trait TicketService {
  
  //Open a ticket
  def open(no: String, title: String): TicketRepository =>  Ticket
  
  //Make a ticket in progress
  def start(no: String): TicketRepository => Ticket 
  
  //Change the title
  def changeTitle(no: String, title: String): TicketRepository => Ticket 
  
  //Close ticket
  def close(no: String): TicketRepository => Ticket
}

//Now our programs have a ticket service and repo
// Let's try to compose them

def program2(s: TicketService, r: TicketRepository) = {
 val chain = (s.open(_:String, "...")(r))
    .andThen(t => s.start(t.no)(r))
    .andThen(t => s.changeTitle(t.no,"New Title")(r))
    .andThen(t => s.close(t.no)(r))
  chain("t1")
}

program2 _

We use the the repository variable in each composition because each method returns a (R => T) (e.g. TicketRepository => Ticket)

Generalizing program3 a reusable function composition:

We need a composition function with signature:

<functionName???>(a,f) :: (R => A) => (A => (R => B)) => (R => B)

If we name the type (of function) (R => A) to RD[R,A]

we need something of type:

<functionName???>(a,f) :: RD[R,A] => (A => RD[R,B]) => RD[R,B]

if we fix the first parameter of R type we actually need something of type:

RD[A] => (A => RD[B]) => RD[B]

which resembles the function flatMap of List[A] but we have now a RD[A]

So we can implemenent a parametric construct RD[R,T] which wraps functions of type R => A and supports a flatMap operation. The implementation of this flatMap composes correctly our enchanced function types.


In [ ]:
//A custom Reader implementation (Reader = RD) in Scala

//Wrapper of functions R => A
case class Reader[R, A](run: R => A) { /* R => A = Function1[R,A] in scala*/

    //Additional map operator 
    def map[B] (f: A => B): Reader[R,B] = {
      Reader(r => f(run(r)))
    }
    
    def flatMap[B] (f: A => Reader[R,B]): Reader[R,B] = {
      Reader(r => f(run(r)).run(r)) // This is just complex function composition boilerplate...
    }
}

Trusting that this implementation is correct we can rewrite our example.


In [ ]:
//Service attempt #3
trait TicketService {
  
  //Open a ticket
  def open(no: String, title: String): Reader[TicketRepository, Ticket]
  
  //Make a ticket in progress
  def start(no: String): Reader[TicketRepository, Ticket]
  
  //Change the title
  def changeTitle(no: String, title: String): Reader[TicketRepository, Ticket] 
  
  //Close ticket
  def close(no: String): Reader[TicketRepository,Ticket]
}

//Now our programs have a ticket service and repo
//Let's try to compose them

def program3(s: TicketService, r: TicketRepository) = {

// Program 2: for reference and comparison
//  val chain = (s.open(_:String, "...")(r))
//     .andThen(t => s.start(t.no)(r))
//     .andThen(t => s.changeTitle(t.no,"....")(r))
//     .andThen(t => s.close(t.no)(r))
//    chain("t1")


  val chain = { (no:String) =>
      s.open(no, "...")
        .flatMap(t => s.start(t.no))
        .flatMap(t => s.changeTitle(t.no, "...."))
        .flatMap(t => s.close(t.no))}
  
  chain("t1").run(r) //Only one usage of r
}

program3 _

Scala has a special syntactic notation for structures that support map and flatMap


In [ ]:
// Service attempt 3.1
def program31(s: TicketService, r: TicketRepository) = { 
  def chain(no: String): Reader[TicketRepository, Ticket] =
    for {
      t <- s.open(no, "...")
      t <- s.start(t.no)
      t <- s.changeTitle(t.no, "....")
      t <- s.close(t.no)
    } yield t
    
  chain("t1").run(r)  
}

program31 _

// Or with a funky name :P 

def openStartChangeTitleAndCloseOperation(no:String, s:TicketService): Reader[TicketRepository, Ticket] = 
  for {
      t <- s.open(no, "...")
      t <- s.start(t.no)
      t <- s.changeTitle(t.no, "....")
      t <- s.close(t.no)
    } yield t

openStartChangeTitleAndCloseOperation _

//invoke on actual code like
// openStartChangeTitleAndCloseOperation("t1",service).run(repo)  where service,repo concrete implementations...

Intoducing Cats

Let's not reinvent the wheel and use a scala functional library


In [ ]:
classpath.add("org.typelevel" %% "cats-core" % "1.0.0-MF")

In [ ]:
// Service attempt #3.2 
// Using Cats

import cats.data.Reader

trait TicketService {
  def open(no: String, title: String): Reader[TicketRepository, Ticket]
  def start(no: String): Reader[TicketRepository, Ticket]
  def changeTitle(no: String, title: String): Reader[TicketRepository, Ticket] 
  def close(no: String): Reader[TicketRepository,Ticket]
}


def program32(no:String, s:TicketService): Reader[TicketRepository, Ticket] = 
  for {
      t <- s.open(no, "...")
      t <- s.start(t.no)
      t <- s.changeTitle(t.no, "....")
      t <- s.close(t.no)
    } yield t

program32 _

Revisiting the repository

  • A real repository may fail (for technical reasons)
  • Tickets may not exist (in the storage)

In [ ]:
import scala.util.Try

// A more realistic repository
trait TicketRepository {
  def query(no: String): Try[Option[Ticket]]
  def store(t: Ticket): Try[Ticket]  
}

Revisiting the service

Our Reader[TicketRepository, Ticket] responces are falling short.


In [ ]:
// Service attempt #4

import scala.util.Try
import cats.data.Kleisli
import cats.implicits._

trait TicketService {
  def open(no: String, title: String): Kleisli[Try, TicketRepository, Ticket]
  def start(no: String): Kleisli[Try, TicketRepository, Ticket]
  def changeTitle(no: String, title: String): Kleisli[Try, TicketRepository, Ticket] 
  def close(no: String): Kleisli[Try, TicketRepository,Ticket]
}

def program4(no:String, s:TicketService): Kleisli[Try, TicketRepository, Ticket] = 
  for {
      t <- s.open(no, "...")
      t <- s.start(t.no)
      t <- s.changeTitle(t.no, "....")
      t <- s.close(t.no)
    } yield t

program4 _

Kleisli is an implementation detail we need something more user friendly name with more domain related meaning.


In [ ]:
// Service attempt #4.1

import scala.util.Try
import cats.data.Kleisli
import cats.implicits._

type ServiceResult[A] = Kleisli[Try,TicketRepository,A]

trait TicketService {
  def open(no: String, title: String): ServiceResult[Ticket]
  def start(no: String): ServiceResult[Ticket]
  def changeTitle(no: String, title: String): ServiceResult[Ticket]
  def close(no: String): ServiceResult[Ticket]
}

def program4(no:String, s:TicketService): ServiceResult[Ticket] = 
  for {
      t <- s.open(no, "...")
      t <- s.start(t.no)
      t <- s.changeTitle(t.no, "....")
      t <- s.close(t.no)
    } yield t

program4 _

Domain interpreters (a.k.a concrete implementations)


In [ ]:
import collection.mutable.{ Map => MMap }

trait InMemoryTicketRepository extends TicketRepository {
  lazy val repo = MMap.empty[String, Ticket]
  
  def query(no: String): Try[Option[Ticket]] = Success(repo.get(no))

  def store(a: Ticket): Try[Ticket] = {
    val r = repo += ((a.no, a))
    Success(a)
  }
}


//This is the concrete implementation
object InMemoryTicketRepository extends InMemoryTicketRepository

We have also to implement a concrete type for our ticket service.


In [ ]:
//package services.interpreters
object TicketService extends TicketService {

  def open(no: String, desc: String) = (r: TicketRepository) =>
      r.query(no) match {
        case Success(Some(t)) => Failure(new Exception(s"Ticket with $no already exists"))
        case Success(None) => 
          //validations
          if (no.isEmpty) Failure(new Exception(s"Ticket $no should not be empty"))
          else if (desc.isEmpty)
          else r.store(Ticket(no, Open, desc, process)) 
        case Failure(ex) => Failure(new Exception(s"Failed to open ticket $no: $desc", ex))
      }
      
      def changeStatus(no: String, status: Ticket) = (r: TicketRepository) => 
        r.query(no) match {
        
        }
  
}

      
//     def changeStatus(no: String, status: TicketStatus): Ticket = ???
   
//     def changeDescription(no: String, descr: String): Ticket = ???
   
//     def close(no: String): Ticket = ???

  
// }


val memoryRepo =  InMemoryTicketRepository 

val TS = TicketService
TS.open("t1", "First ticket",emptyProcess)(memoryRepo)
TS.open("t2", "Second ticket",emptyProcess)(memoryRepo)




memoryRepo.repo

Fotios Paschos, @fpaschos, Sep 2017